[React + Typescript] react-beautiful-dnd を使ってドラッグ&ドロップ機能を実装する
React + Typescript で実装されたフロントエンドにreact-beautiful-dndを使ったドラッグ&ドロップ機能を実装しました。
全体の流れ
本記事でやることの全体の流れです。
- React + Typescript で簡単な画像のリストを作成する
- react-beautiful-dndを導入してリストのアイテムをドラッグ&ドロップできるようにする
成果物
React + Typescript プロジェクトの雛形を作成する
ドラッグ&ドロップを実装する前準備です。
create react app コマンドでプロジェクトの雛形を作成します。言語は Typescript を指定します。
npx create-react-app sample_dd --typescript
画像ファイルを用意する(Optional)
UI 上に表示する画像を準備します。今回はめそこスタンプをドラッグ&ドロップできるようにしました。
public
ディレクトリ下にimages
フォルダを切ってその中に画像データを保持します。
charactersData.ts にドラッグ&ドロップするデータを登録
caractersData.ts
に以下のデータを保持します。
id
は後にreact-beautiful-dnd
を導入する際に必要になるので一意のものを付与してください。
export const CHARACTERS = [ { id: "gambaruzoi", name: "がんばるぞい", thumb: "/images/1.png", }, { id: "gyp", name: "ぎょぱー!", thumb: "/images/2.png", }, { id: "iine", name: "いいね!", thumb: "/images/3.png", }, { id: "shincyoku_doudesuka", name: "進捗どうですか", thumb: "/images/4.png", }, { id: "shobon", name: "ショボーン", thumb: "/images/5.png", }, ];
App.tsx にリストを追加する
header と ul 要素 を App.tsx
に追加します。先ほど作成したcharactersData.ts
をインポートして、map で要素を追加します。
import { CHARACTERS } from "./charactersData";
<div className="App"> <header className="App-header"> <h1>めそこスタンプ</h1> <ul className="characters"> {CHARACTERS.map(({ id, name, thumb }) => { return ( <li key={id}> <div className="characters-thumb"> <img src={thumb} alt={`${name} Thumb`} /> </div> <p>{name}</p> </li> ); })} </ul> </header> </div>
この時点で以下の画面が完成しました。このリスト内の要素をドラッグドロップで移動できるように実装を進めます。
react-beautiful-dnd ライブラリと型をインストール
react-beautiful-dnd
ライブラリをインストールします。
yarn add @types/react-beautiful-dnd yarn add react-beautiful-dnd
DragDropContext を App の root に追加する
import { DragDropContext } from "react-beautiful-dnd";
DragDropContext
を React アプリのソースコードの最上位に追加することでreact-beautiful-dnd
がコンポーネントツリーにアクセスできるようになります。今回はApp.tsx
に全てのコードを記述しているのでApp.tsx
にDragDropContext
を追加します。
複数のコンポーネントでドラッグ&ドロップを利用する場合も同様で、ソースコードの最上位にDragDropContext
が追加され、その下のコンポーネントをラップしている必要があります。
<div className="App"> <header className="App-header"> <h1>めそこスタンプ</h1> <DragDropContext> <ul className="characters"> {CHARACTERS.map(({id, name, thumb}) => { return ( <li key={id}> <div className="characters-thumb"> <img src={thumb} alt={`${name} Thumb`} /> </div> <p> { name } </p> </li> ); })} </ul> <DragDropContext> </header> </div>
ul 要素を Droppable エリアにする
import { DragDropContext, Droppable } from "react-beautiful-dnd";
次に、ドラッグしたアイテムをドロップできる範囲を追加します。
今回は<ul></ul>
の範囲にアイテムをドロップできるようにしたいのでDroppable
を以下の位置に追加します。
<div className="App"> <header className="App-header"> <h1>めそこスタンプ</h1> <DragDropContext> {/* Droppableをここに追加 */} <Droppable droppableId="characters"> {(provided) => ( <ul className="characters" {...provided.droppableProps} ref={provided.innerRef}> {CHARACTERS.map(({id, name, thumb}) => { return ( <li key={id}> <div className="characters-thumb"> <img src={thumb} alt={`${name} Thumb`} /> </div> <p> { name } </p> </li> ); })} </ul> )} </Droppable> <DragDropContext> </header> </div>
Droppable
にはdroppableId
を追加します。これによりライブラリがこの特定の Droppable インスタンスを追跡できるようになります。
また、provided
引数を渡す関数でラップする必要があり、この引数に含まれる値を元にどのアイテムがどの位置に移動されたかをトラッキングします。その部分が以下のコードになります。
<ul className="characters" {...provided.droppableProps} ref="{provided.innerRef}" ></ul>
リスト 内のアイテムをドラッグ可能にする Draggable を追加
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
ドラッグ可能にしたいアイテムをDraggable
コンポーネントでラップします。
Draggable
も先ほどのDroppable
と同様draggableId
を付与する必要があります。ここにはcharactersData.ts
であらかじめ設定しておいたid
を付与します。<li>
に付与していたkey
プロパティもDraggable
コンポーネントへ移動します。
また、<li>
要素にもDraggable
コンポーネントから渡されるprovided
引数の以下のプロパティを付与します。
<li ref="{provided.innerRef}" {...provided.draggableProps} {...provided.dragHandleProps} ></li>
<div className="App"> <header className="App-header"> <h1>めそこスタンプ</h1> <DragDropContext> {/* Droppableをここに追加 */} <Droppable droppableId="characters"> {(provided) => ( <ul className="characters" {...provided.droppableProps} ref={provided.innerRef}> {CHARACTERS.map(({id, name, thumb}, index) => { return ( {/* Draggableをここに追加 */} <Draggable key={id} draggableId={id} index={index}> {(provided) => ( <li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}> <div className="characters-thumb"> <img src={thumb} alt={`${name} Thumb`} /> </div> <p> { name } </p> </li> )} </Draggable> ); })} </ul> )} </Droppable> <DragDropContext> </header> </div>
ここまででドラッグ&ドロップ”っぽい”動きをするアプリができました。 ですが、このままでは実際に動かしてみるとドロップしたアイテムの順番が入れ替わってしまいます。
これは以下の2つの問題が原因で起こります。
- アイテムをドラッグした際、そのアイテムが以前あった位置に次のアイテムが移動してしまう
- React コンポーネントのレンダリングのタイミングでライブラリ内に保持されていたアイテムの順序情報が消えてしまう
placeholder を追加する
React Beautiful DnD が提供するplaceholder
をDroppable
下に追加します。
placeholder
を追加することで、ドラッグしたアイテムがドラッグされる前に使っていたスペースを埋めてくれるので、ドラッグした際にその下のアイテムがドラッグしたアイテムが以前あった場所に移動してしまう挙動が解決します。
<div className="App"> <header className="App-header"> <h1>めそこスタンプ</h1> <DragDropContext> {/* Droppableをここに追加 */} <Droppable droppableId="characters"> {(provided) => ( <ul className="characters" {...provided.droppableProps} ref={provided.innerRef}> {CHARACTERS.map(({id, name, thumb}, index) => { return ( {/* Draggableをここに追加 */} <Draggable key={id} draggableId={id} index={index}> {(provided) => ( <li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}> <div className="characters-thumb"> <img src={thumb} alt={`${name} Thumb`} /> </div> <p> { name } </p> </li> )} </Draggable> ); })} {/* placeholderをここに追加 */} {provided.placeholder} </ul> )} </Droppable> </DragDropContext> </header> </div>
state と onDragEnd 関数を設定してドロップ後の List 内アイテムの順番を保持する
今の実装では、ドラッグ&ドロップしたアイテムが1つ前の位置に戻ってしまいます。これはドラッグ&ドロップした後、コンポーネントが再レンダリングされた時にreact-beautiful-dnd
のメモリ内に保持しているアイテムの順序が消えてしまうことが原因です。
これを回避するためにコンポーネントのstate
に移動後のアイテムの情報を保持するように state を追加します。
さらに、アイテムがドロップされた後に実行される関数、onDragEnd
にhandleOnDragEnd
という関数を渡して上記のstate
を更新する処理を記述します。
import React, { useState } from "react"; import "./App.css"; import { CHARACTERS } from "./charactersData"; import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; function App() { const [characters, updateCharacters] = useState(CHARACTERS); function handleOnDragEnd(result: any) { const items = Array.from(characters); const [reorderedItem] = items.splice(result.source.index, 1); items.splice(result.destination.index, 0, reorderedItem); updateCharacters(items); } return ( <div className="App"> <header className="App-header"> <h1>めそこスタンプ</h1> <DragDropContext onDragEnd={handleOnDragEnd}> <Droppable droppableId="characters"> {(provided) => ( <ul className="characters" {...provided.droppableProps} ref={provided.innerRef} > {characters.map(({ id, name, thumb }, index) => { return ( <Draggable key={id} draggableId={id} index={index}> {(provided) => ( <li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} > <div className="characters-thumb"> <img src={thumb} alt={`${name} Thumb`} /> </div> <p>{name}</p> </li> )} </Draggable> ); })} {provided.placeholder} </ul> )} </Droppable> </DragDropContext> </header> </div> ); } export default App;
これでreact-beautiful-dnd
を使ったドラッグ&ドロップ機能の実装が完了しました。
npm trends で見る react-beautiful-dnd
npm trendsで類似のライブラリを比べると一番利用されているのはreact-draggable、次いでreact-beautiful-dnd、react-dndというデータが表示されました。
今回は特別なことは何もせず、リストに入ったアイテムを移動したいという簡単な要件を満たしたいだけだったので塾考せずにサンプルが多い&ドキュメントが可愛らしいという理由でreact-beautiful-dnd
を選択しましたが、ライブラリは要件に合わせて選出するのが良さそうです。